﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ICSharpCode.NRefactory;
using System.Diagnostics;
using ICSharpCode.NRefactory.Ast;
using System.IO;
using ICSharpCode.NRefactory.Parser;
using System.Xml.Linq;

namespace Cs2as
{
    public class Program
    {
        internal string OutDir;
        internal List<MethodDeclaration> ConvertingExtensionMethods;
        internal List<XDocument> TranslationDocs;
        internal HashSet<string> EnumNames;
        internal HashSet<string> DelegateNames;
        internal int InLambda = 0;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sourceFiles">cs files to parse</param>
        /// <param name="outDir">Folder to write ActionScript files to. Existing files will be overwritten.</param>
        /// <param name="conditionalCompilationSymbols">Pre-processor conditions to obey. CS2AS is automatically defined.</param>
        /// <param name="extraTranslation">Path to xml files to supplement Translations.xml with.  Users can define their own translations specific to their project here.</param>
        public void Go(IEnumerable<string> sourceFiles, string outDir, IEnumerable<string> conditionalCompilationSymbols, IEnumerable<string> extraTranslation)
        {

            TranslationDocs = Translations.Translation.BuildTranslationDocs(extraTranslation);

            OutDir = outDir;
            if (!Directory.Exists(OutDir))
                Directory.CreateDirectory(outDir);

            var parsers = sourceFiles.ToDictionary(o => o, o => ParserFactory.CreateParser(o));

            Console.WriteLine("Parsing...");
            foreach (var parser in parsers.Values)
            {
                parser.Lexer.EvaluateConditionalCompilation = true;
                foreach (var symbol in conditionalCompilationSymbols)
                    parser.Lexer.ConditionalCompilationSymbols.Add(symbol, null);
                if (!conditionalCompilationSymbols.Any(o => o == "CS2AS"))
                    parser.Lexer.ConditionalCompilationSymbols.Add("CS2AS", null);

                parser.Lexer.SkipAllComments = false;

                parser.Parse();
                parser.Dispose(); //Dispose it so it releases the write lock on the source file.
            }

            Console.WriteLine("Done parsing.  Generating ActionScript...");

            var allNamespaces = parsers.Values
                .SelectMany(o => o.CompilationUnit.Children.OfType<NamespaceDeclaration>())
                .GroupBy(o => o.Name)
                .Select(o => new { NamespaceName = o.Key, Namespaces = o });

            var typesGroupedByNamespace = allNamespaces.Select(o => o.Namespaces.SelectMany(n => n.Children.OfType<TypeDeclaration>())
                .GroupBy(t => t.Name)
                .Select(t => new { TypeName = t.Key, Partials = t }));

            var allTypes = typesGroupedByNamespace.SelectMany(o => o).SelectMany(o => o.Partials);

            ConvertingExtensionMethods = typesGroupedByNamespace.SelectMany(o => o).SelectMany(o => o.Partials)
                .SelectMany(o => o.Children)
                .OfType<MethodDeclaration>()
                .Where(o => o.IsExtensionMethod)
                .ToList();

            EnumNames = allTypes.Where(o => o.Type == ClassType.Enum).Select(o => o.Name).ToHashSet(true);

            DelegateNames = allNamespaces.SelectMany(o => o.Namespaces).SelectMany(o => o.Children)
                .OfType<DelegateDeclaration>().Select(o => o.Name)
                .Concat(allTypes.SelectMany(o => o.Children).OfType<DelegateDeclaration>().Select(o => o.Name))
                .ToHashSet(false);

            foreach (var ns in typesGroupedByNamespace)
                foreach (var type in ns)
                    GenerateType(type.TypeName, type.Partials);

            Console.WriteLine("Done.");
        }

        private void GenerateType(string typeName, IEnumerable<TypeDeclaration> partials)
        {
            var first = partials.First();
            var typeNamespace = first.Parent.As<NamespaceDeclaration>();

            Func<Modifiers, bool> hasModifier = mod => partials.Any(o => o.Modifier.Has(mod));

            var dir = Path.Combine(OutDir, typeNamespace.Name.Replace(".", Path.DirectorySeparatorChar.ToString()));
            if (!Directory.Exists(dir))
                Directory.CreateDirectory(dir);

            string destFile = Path.Combine(dir, typeName + ".as");

            using (var writer = new ActionScriptWriter(File.CreateText(destFile)))
            {
                writer.WriteLine("package " + typeNamespace.Name);
                writer.WriteOpenBrace();

                foreach (var usingDeclaration in 
                    partials.SelectMany(o => o.Parent.Parent.Children.OfType<UsingDeclaration>())
                    .SelectMany(o => o.Usings)
                    .Select(o => o.Name)
                    .Distinct()
                    .OrderBy(o => o))
                {
                    writer.WriteLine("import " + usingDeclaration + ".*;");
                }
                writer.WriteLine("import flash.utils.ByteArray;");


                bool isEnum = first.Type == ClassType.Enum;
                bool derivesFromObject = true;

                writer.WriteIndent();
                if (hasModifier(Modifiers.Public))
                    writer.Write("public ");
                if (isEnum || hasModifier(Modifiers.Static))
                    writer.Write("final ");

                switch (first.Type)
                {
                    case ClassType.Class:
                    case ClassType.Struct:
                    case ClassType.Enum:
                        writer.Write("class ");
                        break;
                    case ClassType.Interface:
                        writer.Write("interface ");
                        break;
                    default:
                        throw new Exception("Need handler for " + first.Type);
                }
                writer.Write(typeName);

                if (!isEnum)
                {
                    writer.Write(" ");
                    bool firstImplements = true;

                    foreach (var baseType in partials.SelectMany(o => o.BaseTypes))
                    {
                        //Assume anything that starts with I-Upper is an interface
                        bool isInterface = baseType.Type.StartsWith("I") && char.IsUpper(baseType.Type[1]);

                        if (isInterface)
                        {
                            if (firstImplements)
                            {
                                writer.Write(" implements ");
                                firstImplements = false;
                            }
                            else
                                writer.Write(", ");

                            writer.Write(baseType.Type);
                        }
                        else
                        {
                            writer.Write("extends ");
                            writer.Write(baseType.Type);
                            derivesFromObject = false;
                        }
                    }
                }

                writer.Write("\r\n");

                writer.WriteOpenBrace();

                var allChildren = partials.SelectMany(o => o.Children);
                bool hasStaticConstructor = false;

                if (isEnum)
                {
                    GenerateEnumBody(writer, allChildren);
                }
                else
                {
                    GenerateFields(writer, allChildren.OfType<FieldDeclaration>());
                    writer.WriteLine();
                    GenerateProperties(writer, allChildren.OfType<PropertyDeclaration>());
                    writer.WriteLine();
                    hasStaticConstructor = GenerateConstructors(writer, allChildren.OfType<ConstructorDeclaration>());
                    writer.WriteLine();
                    GenerateMethods(writer, allChildren.OfType<MethodDeclaration>(), derivesFromObject);
                }

                writer.WriteCloseBrace();

                if (hasStaticConstructor)
                    writer.WriteLine(typeName + ".cctor();");

                writer.WriteCloseBrace();
            }
        }

        private void GenerateEnumBody(ActionScriptWriter writer, IEnumerable<INode> allChildren)
        {
            int lastEnumValue = 0;
            foreach (FieldDeclaration field in allChildren)
            {
                var varDeclaration = field.Fields.Single();

                writer.WriteIndent();
                writer.Write("public static const ");
                writer.Write(varDeclaration.Name);
                writer.Write(":int = ");

                if (varDeclaration.Initializer.IsNull)
                    lastEnumValue++;
                else
                {
                    bool minus = false;
                    var initializer = varDeclaration.Initializer;

                    if (initializer is UnaryOperatorExpression)
                    {
                        var unary = varDeclaration.Initializer.As<UnaryOperatorExpression>();
                        if (unary.Op == UnaryOperatorType.Minus)
                        {
                            minus = true;
                            initializer = unary.Expression;
                        }
                    }

                    if (!(initializer is PrimitiveExpression))
                        throw new Exception("Enums can only be initalized with constants.");

                    lastEnumValue = (int)initializer.As<PrimitiveExpression>().Value;

                    if (minus)
                        lastEnumValue = -lastEnumValue;
                }

                writer.Write(lastEnumValue.ToString());
                writer.Write(";\r\n");
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="constructors"></param>
        /// <returns>True if the type has a static constructor, otherwise false</returns>
        private bool GenerateConstructors(ActionScriptWriter writer, IEnumerable<ConstructorDeclaration> constructors)
        {
            var staticConstructor = constructors.SingleOrDefault(o => o.Modifier.Has(Modifiers.Static));

            if (staticConstructor != null)
                WriteStaticConstructor(writer, staticConstructor);

            var normalctors = constructors.Except(staticConstructor);

            if (normalctors.Count() > 1)
                throw new Exception("Overloaded constructors not allowed: " + Utility.Descriptor(normalctors.First()));

            if (normalctors.Count() == 0)
                return staticConstructor != null;

            var ctor = normalctors.Single();

            writer.WriteIndent();

            writer.Write("public function ");
            writer.Write(ctor.Name);
            writer.Write("(");

            var firstParameter = true;
            foreach (var parameter in ctor.Parameters)
            {
                if (firstParameter)
                    firstParameter = false;
                else
                    writer.Write(", ");

                writer.Write(parameter.ParameterName);
                writer.Write(":");
                writer.Write(ConvertType(parameter.TypeReference));
            }

            writer.Write(")\r\n");
            writer.WriteOpenBrace();
            WriteStatement(writer, ctor.Body);
            writer.WriteCloseBrace();

            return staticConstructor != null;
        }

        private void WriteStaticConstructor(ActionScriptWriter writer, ConstructorDeclaration staticConstructor)
        {
            writer.WriteLine("internal static function cctor():void");
            writer.WriteOpenBrace();
            WriteStatement(writer, staticConstructor.Body);
            writer.WriteCloseBrace();
        }

        private void GenerateProperties(ActionScriptWriter writer, IEnumerable<PropertyDeclaration> properties)
        {
            foreach (var property in properties)
            {
                Action<PropertyGetSetRegion, bool> writeRegion = (region, get) =>
                    {
                        Func<Modifiers, bool> hasModifier = mod => region.Modifier.Has(mod) || property.Modifier.Has(mod);

                        writer.WriteIndent();

                        if (hasModifier(Modifiers.Override))
                            writer.Write("override ");
                        if (hasModifier(Modifiers.Public))
                            writer.Write("public ");
                        if (hasModifier(Modifiers.Protected))
                            writer.Write("protected ");
                        if (hasModifier(Modifiers.Private))
                            writer.Write("private ");
                        if (hasModifier(Modifiers.Internal))
                            writer.Write("internal ");
                        if (hasModifier(Modifiers.Static))
                            writer.Write("static ");

                        writer.Write("function ");
                        writer.Write(get ? "get " : "set ");
                        writer.Write(property.Name);

                        string type = ConvertType(property.TypeReference);

                        if (get)
                            writer.Write("():" + type);
                        else
                            writer.Write("(value:" + type + "):void");

                        writer.WriteLine();
                        writer.WriteOpenBrace();

                        if (hasModifier(Modifiers.Abstract))
                            writer.WriteLine("throw new Error(\"Abstract item called\");");
                        else
                            WriteStatement(writer, region.Block);

                        writer.WriteCloseBrace();
                        writer.WriteLine();
                    };

                if (property.HasGetRegion)
                    writeRegion(property.GetRegion, true);
                if (property.HasSetRegion)
                    writeRegion(property.SetRegion, false);
            }
        }

        private void GenerateFields(ActionScriptWriter writer, IEnumerable<FieldDeclaration> fields)
        {
            foreach (var field in fields)
            {
                foreach (var declaration in field.Fields)
                {
                    writer.WriteIndent();
                    if (field.Modifier.Has(Modifiers.Public))
                        writer.Write("public ");
                    if (field.Modifier.Has(Modifiers.Protected))
                        writer.Write("protected ");
                    if (field.Modifier.Has(Modifiers.Private))
                        writer.Write("private ");
                    if (field.Modifier.Has(Modifiers.Internal))
                        writer.Write("internal ");
                    if (field.Modifier.Has(Modifiers.Static))
                        writer.Write("static ");

                    if (field.Modifier.Has(Modifiers.Const))
                    {
                        if (!field.Modifier.Has(Modifiers.Public) && !field.Modifier.Has(Modifiers.Private) && !field.Modifier.Has(Modifiers.Protected) && !field.Modifier.Has(Modifiers.Internal))
                            writer.Write("private ");

                        writer.Write("static const ");
                    }
                    else
                        writer.Write("var ");

                    writer.Write(declaration.Name);
                    writer.Write(":");
                    writer.Write(ConvertType(field.TypeReference));

                    if (!declaration.Initializer.IsNull)
                    {
                        writer.Write(" = ");
                        WriteStatement(writer, declaration.Initializer);
                    }

                    writer.Write(";");
                    writer.WriteLine();
                }
            }
        }

        private void GenerateMethods(ActionScriptWriter writer, IEnumerable<MethodDeclaration> methods, bool typeDerivesFromObject)
        {
            foreach (var overloadedGroup in methods.GroupBy(o => o.Name))
            {
                //Find the primary method
                var method = overloadedGroup.First(o => o.Parameters.Count == overloadedGroup.Max(m => m.Parameters.Count));

                var defaultParameters = Enumerable.Range(0, method.Parameters.Count).Select(o => (string)null).ToList();

                foreach (var overload in overloadedGroup.Where(o => o != method))
                {
                    Action err = () => { throw new Exception("Overloads must not do anything other than call the primary method: " + Utility.Descriptor(overload)); };
                    //Each overload must resolve to the primary method
                    if (overload.Body.Children.Count > 1)
                        err();
                    var stmt = overload.Body.Children.Single();

                    if (stmt is ReturnStatement)
                        stmt = stmt.As<ReturnStatement>().Expression;
                    else
                    {
                        if (!(stmt is ExpressionStatement))
                            err();
                        stmt = stmt.As<ExpressionStatement>().Expression;
                    }

                    if (!(stmt is InvocationExpression))
                        err();
                    var args = stmt.As<InvocationExpression>().Arguments;
                    if (args.Count != method.Parameters.Count)
                        err();
                    for (int i = 0; i < args.Count; i++)
                    {
                        if (args[i] is PrimitiveExpression)
                            defaultParameters[i] = args[i].As<PrimitiveExpression>().StringValue;
                        else if (!(args[i] is IdentifierExpression))
                            err();
                    }
                }

                writer.WriteIndent();

                if ((method.Name != "ToString" || !typeDerivesFromObject) && method.Modifier.Has(Modifiers.Override))
                    writer.Write("override ");
                if (method.Modifier.Has(Modifiers.Public))
                    writer.Write("public ");
                if (method.Modifier.Has(Modifiers.Private))
                    writer.Write("private ");
                if (method.Modifier.Has(Modifiers.Protected))
                    writer.Write("protected ");
                if (method.Modifier.Has(Modifiers.Internal))
                    writer.Write("internal ");
                if (method.Modifier.Has(Modifiers.Static))
                    writer.Write("static ");

                writer.Write("function ");

                if (method.Name == "ToString")
                    writer.Write("toString");
                else
                    writer.Write(method.Name);

                writer.Write("(");

                var parameterNumber = 0;
                foreach (var parameter in method.Parameters)
                {
                    if (parameter.ParamModifier.Has(ParameterModifiers.Out))
                        throw new Exception("out is not supported: " + Utility.Descriptor(method));
                    if (parameter.ParamModifier.Has(ParameterModifiers.Ref))
                        throw new Exception("ref is not supported: " + Utility.Descriptor(method));

                    if (parameterNumber > 0)
                        writer.Write(", ");

                    writer.Write(parameter.ParameterName);
                    writer.Write(":");

                    if (method.Templates.Any(o => o.Name == parameter.TypeReference.Type))
                        writer.Write("*"); //Generic parameter
                    else
                        writer.Write(ConvertType(parameter.TypeReference));

                    var def = defaultParameters[parameterNumber];
                    if (def != null)
                        writer.Write(" = " + def);

                    parameterNumber++;
                }

                writer.Write("):");

                if (method.Templates.Any(o => o.Name == method.TypeReference.Type))
                    writer.Write("*"); //method returns one of its generic types
                else
                    writer.Write(ConvertType(method.TypeReference));

                if (method.Modifier.Has(Modifiers.Abstract))
                {
                    writer.WriteLine();
                    writer.WriteOpenBrace();
                    writer.WriteLine("throw new Error(\"Abstract item called\");");
                    writer.WriteCloseBrace();
                }
                else if (method.Body.IsNull)
                    writer.Write(";\r\n"); //interface methods
                else
                {
                    writer.WriteLine();
                    writer.WriteOpenBrace();
                    foreach (var statement in method.Body.Children)
                        WriteStatement(writer, statement);
                    writer.WriteCloseBrace();
                }
            }
        }


        private void WriteStatement(ActionScriptWriter writer, INode statement)
        {
            WriteStatement(writer, statement, false);
        }
        private void WriteStatement(ActionScriptWriter writer, INode statement, bool suppressSemicolon)
        {
            if (statement is ExpressionStatement)
                WriteExpressionStatement(writer, statement.As<ExpressionStatement>(), suppressSemicolon);
            else if (statement is IdentifierExpression)
                writer.Write(statement.As<IdentifierExpression>().Identifier);
            else if (statement is PrimitiveExpression)
                WritePrimitiveExpression(writer, statement.As<PrimitiveExpression>());
            else if (statement is LocalVariableDeclaration)
                WriteLocalVariableDeclaration(writer, statement.As<LocalVariableDeclaration>(), suppressSemicolon);
            else if (statement is IfElseStatement)
                WriteIfElseStatement(writer, statement.As<IfElseStatement>());
            else if (statement is BinaryOperatorExpression)
                WriteBinaryOperatorExpression(writer, statement.As<BinaryOperatorExpression>());
            else if (statement is InvocationExpression)
                WriteInvocationExpression(writer, statement.As<InvocationExpression>());
            else if (statement is MemberReferenceExpression)
                WriteMemberReferenceExpression(writer, statement.As<MemberReferenceExpression>());
            else if (statement is UnaryOperatorExpression)
                WriteUnaryOperatorExpression(writer, statement.As<UnaryOperatorExpression>());
            else if (statement is ReturnStatement)
                WriteReturnStatement(writer, statement.As<ReturnStatement>());
            else if (statement is AssignmentExpression)
                WriteAssignmentExpression(writer, statement.As<AssignmentExpression>());
            else if (statement is ArrayCreateExpression)
                WriteArrayCreateExpression(writer, statement.As<ArrayCreateExpression>());
            else if (statement is ForeachStatement)
                WriteForeachStatement(writer, statement.As<ForeachStatement>());
            else if (statement is BlockStatement)
                WriteBlockStatement(writer, statement.As<BlockStatement>());
            else if (statement is IndexerExpression)
                WriteIndexerExpression(writer, statement.As<IndexerExpression>());
            else if (statement is DoLoopStatement)
                WriteDoLoopStatement(writer, statement.As<DoLoopStatement>());
            else if (statement is BreakStatement)
                WriteBreakStatement(writer, statement.As<BreakStatement>());
            else if (statement is ForStatement)
                WriteForStatement(writer, statement.As<ForStatement>());
            else if (statement is LambdaExpression)
                WriteLambdaExpression(writer, statement.As<LambdaExpression>());
            else if (statement is CastExpression)
                WriteCastExpression(writer, statement.As<CastExpression>());
            else if (statement is ParenthesizedExpression)
                WriteParenthesizedExpression(writer, statement.As<ParenthesizedExpression>());
            else if (statement is ObjectCreateExpression)
                WriteObjectCreateExpression(writer, statement.As<ObjectCreateExpression>());
            else if (statement is CheckedStatement)
                WriteCheckedStatement(writer, statement.As<CheckedStatement>());
            else if (statement is UsingStatement)
                WriteUsingStatement(writer, statement.As<UsingStatement>());
            else if (statement is ThrowStatement)
                WriteThrowStatement(writer, statement.As<ThrowStatement>());
            else if (statement is TryCatchStatement)
                WriteTryCatchStatement(writer, statement.As<TryCatchStatement>());
            else if (statement is BaseReferenceExpression)
                WriteBaseReferenceExpression(writer, statement.As<BaseReferenceExpression>());
            else if (statement is TypeOfIsExpression)
                WriteTypeOfIsExpression(writer, statement.As<TypeOfIsExpression>());
            else if (statement is ConditionalExpression)
                WriteConditionalExpression(writer, statement.As<ConditionalExpression>());
            else if (statement is ThisReferenceExpression)
                WriteThisReferenceExpression(writer, statement.As<ThisReferenceExpression>());
            else if (statement is SwitchStatement)
                WriteSwitchStatement(writer, statement.As<SwitchStatement>());
            else if (statement is ContinueStatement)
                WriteContinueStatement(writer, statement.As<ContinueStatement>());
            else if (statement is TypeOfExpression)
                WriteTypeOfExpression(writer, statement.As<TypeOfExpression>());
            else if (statement is LockStatement)
                WriteLockStatement(writer, statement.As<LockStatement>());
            else if (statement is EmptyStatement)
            {
                //Eat empty statements
            }
            else if (statement is DirectionExpression || statement is YieldStatement)
                throw new ArgumentException(statement.GetType().Name + " is not supported.  Found in " + Utility.Descriptor(statement));

            else
                throw new Exception("Need handler for " + statement.GetType().Name + " at " + Utility.Descriptor(statement));
        }

        private void WriteLockStatement(ActionScriptWriter writer, LockStatement lockStatement)
        {
            //Eat lock statements - ActionScript is unfortunately single-threaded.
            WriteStatement(writer, lockStatement.EmbeddedStatement);
        }

        private void WriteTypeOfExpression(ActionScriptWriter writer, TypeOfExpression typeOfExpression)
        {
            writer.Write("typeof(");
            writer.Write(typeOfExpression.TypeReference.Type);
            writer.Write(")");
        }

        private void WriteContinueStatement(ActionScriptWriter writer, ContinueStatement continueStatement)
        {
            writer.WriteLine("continue;");
        }

        private void WriteThisReferenceExpression(ActionScriptWriter writer, ThisReferenceExpression thisReferenceExpression)
        {
            if (InLambda > 0)
                throw new InvalidOperationException("Cannot use \"this\" in a lambda.  Simply remove the \"this\" and use the field directly.  If you must access the \"this\" pointer, you must create a reference to it outside the lambda. Found at " + Utility.Descriptor(thisReferenceExpression));
            writer.Write("this");
        }

        private void WriteSwitchStatement(ActionScriptWriter writer, SwitchStatement switchStatement)
        {
            writer.WriteIndent();
            writer.Write("switch (");
            WriteStatement(writer, switchStatement.SwitchExpression);
            writer.Write(")\r\n");
            writer.WriteOpenBrace();
            foreach (var section in switchStatement.SwitchSections)
            {
                foreach (var label in section.SwitchLabels)
                {
                    writer.WriteIndent();

                    if (label.IsDefault)
                        writer.Write("default");
                    else
                    {
                        writer.Write("case ");
                        WriteStatement(writer, label.Label);
                    }

                    writer.Write(":\r\n");
                }
                writer.Indent++;

                foreach (var child in section.Children)
                    WriteStatement(writer, child);

                writer.Indent--;
            }
            writer.WriteCloseBrace();
        }

        private void WriteConditionalExpression(ActionScriptWriter writer, ConditionalExpression conditionalExpression)
        {
            WriteStatement(writer, conditionalExpression.Condition);
            writer.Write(" ? ");
            WriteStatement(writer, conditionalExpression.TrueExpression);
            writer.Write(" : ");
            WriteStatement(writer, conditionalExpression.FalseExpression);
        }

        private void WriteTypeOfIsExpression(ActionScriptWriter writer, TypeOfIsExpression typeOfIsExpression)
        {
            WriteStatement(writer, typeOfIsExpression.Expression);
            writer.Write(" is ");
            writer.Write(ConvertType(typeOfIsExpression.TypeReference));
        }

        private void WriteBaseReferenceExpression(ActionScriptWriter writer, BaseReferenceExpression baseReferenceExpression)
        {
            writer.Write("super");
        }

        private void WriteTryCatchStatement(ActionScriptWriter writer, TryCatchStatement tryCatchStatement)
        {
            writer.WriteLine("try");
            writer.WriteOpenBrace();
            WriteStatement(writer, tryCatchStatement.StatementBlock);
            writer.WriteCloseBrace();

            foreach (var catchClause in tryCatchStatement.CatchClauses)
            {
                string varName = catchClause.VariableName;

                if (string.IsNullOrEmpty(varName))
                    varName = "noVarName_" + Guid.NewGuid().ToString("N");

                writer.WriteIndent();
                writer.Write("catch (");
                writer.Write(varName);
                writer.Write(":");
                writer.Write(ConvertType(catchClause.TypeReference));
                writer.Write(")\r\n");
                writer.WriteOpenBrace();
                WriteStatement(writer, catchClause.StatementBlock);
                writer.WriteCloseBrace();
            }

            if (!tryCatchStatement.FinallyBlock.IsNull)
            {
                writer.WriteLine("finally");
                writer.WriteOpenBrace();
                WriteStatement(writer, tryCatchStatement.FinallyBlock);
                writer.WriteCloseBrace();
            }
        }

        private void WriteThrowStatement(ActionScriptWriter writer, ThrowStatement throwStatement)
        {
            writer.WriteIndent();
            writer.Write("throw");
            if (!throwStatement.Expression.IsNull)
            {
                writer.Write(" ");
                WriteStatement(writer, throwStatement.Expression);
            }
            writer.Write(";\r\n");
        }

        private void WriteUsingStatement(ActionScriptWriter writer, UsingStatement usingStatement)
        {
            INode resourceAcquisition = usingStatement.ResourceAcquisition;
            if (resourceAcquisition is ExpressionStatement)
                resourceAcquisition = resourceAcquisition.As<ExpressionStatement>().Expression;

            //Ensure the using statement is a local variable - we can't deal with things we can't reliably repeat in the finally block
            if (!(resourceAcquisition is IdentifierExpression))
                throw new Exception("Using statements must reference a local variable. " + Utility.Descriptor(usingStatement));

            writer.WriteLine("try");
            writer.WriteOpenBrace();
            WriteStatement(writer, usingStatement.EmbeddedStatement);
            writer.WriteCloseBrace();
            writer.WriteLine("finally");
            writer.WriteOpenBrace();
            writer.WriteIndent();
            WriteStatement(writer, resourceAcquisition);
            writer.Write(".Dispose();\r\n");
            writer.WriteCloseBrace();
        }

        private void WriteCheckedStatement(ActionScriptWriter writer, CheckedStatement checkedStatement)
        {
            WriteStatement(writer, checkedStatement.Block);
        }

        private void WriteObjectCreateExpression(ActionScriptWriter writer, ObjectCreateExpression objectCreateExpression)
        {
            if (!objectCreateExpression.ObjectInitializer.IsNull)
                throw new Exception("C# 3.5 object initialization syntax is not supported. " + Utility.Descriptor(objectCreateExpression));

            var translate = Translations.Translation.GetTranslation(TranslationDocs, Translations.Translation.TranslationType.Method, ".ctor", objectCreateExpression.CreateType) as Translations.Method;

            writer.Write("new ");
            writer.Write(ConvertType(objectCreateExpression.CreateType));
            writer.Write("(");

            var firstParameter = true;
            foreach (var parameter in TranslateParameters(translate, objectCreateExpression.Parameters, null))
            {
                if (firstParameter)
                    firstParameter = false;
                else
                    writer.Write(", ");

                WriteStatement(writer, parameter);
            }

            writer.Write(")");
        }

        private IEnumerable<Expression> TranslateParameters(Translations.Translation translate, List<Expression> list, InvocationExpression invoke)
        {
            if (translate == null)
                return list;
            else if (translate is Translations.Method)
                return translate.As<Translations.Method>().TranslateParameters(list, invoke, this);
            else
                throw new Exception("Need handler for " + translate.GetType().Name);
        }


        private void WriteParenthesizedExpression(ActionScriptWriter writer, ParenthesizedExpression parenthesizedExpression)
        {
            writer.Write("(");
            WriteStatement(writer, parenthesizedExpression.Expression);
            writer.Write(")");
        }

        private void WriteCastExpression(ActionScriptWriter writer, CastExpression castExpression)
        {
            switch (castExpression.CastType)
            {
                case CastType.Cast:
                    var destType = ConvertType(castExpression.CastTo);

                    if (destType == "Array" || destType == "Function") //For some reason, ActionScript does not seem to like casts to arrays or functions.
                        WriteStatement(writer, castExpression.Expression);
                    else
                    {
                        writer.Write(destType);
                        writer.Write("(");
                        WriteStatement(writer, castExpression.Expression);
                        writer.Write(")");
                    }
                    break;
                case CastType.TryCast:
                    WriteStatement(writer, castExpression.Expression);
                    writer.Write(" as ");
                    writer.Write(ConvertType(castExpression.CastTo));
                    break;
                default:
                    throw new Exception("Need handler for " + castExpression.CastType);
            }
        }

        private void WriteLambdaExpression(ActionScriptWriter writer, LambdaExpression lambdaExpression)
        {
            string returnType = "*";
            string[] parameterTypes = lambdaExpression.Parameters.Select(o => "*").ToArray();

            if (lambdaExpression.Parent.Parent is LocalVariableDeclaration)
            {
                var lambdaType = lambdaExpression.Parent.Parent.As<LocalVariableDeclaration>().TypeReference;

                if (lambdaType.Type == "Action")
                {
                    returnType = "void";
                    parameterTypes = lambdaType.GenericTypes.Select(o => ConvertType(o)).ToArray();
                }
                else if (lambdaType.Type == "Func")
                {
                    returnType = ConvertType(lambdaType.GenericTypes.Last());
                    parameterTypes = lambdaType.GenericTypes.Take(lambdaType.GenericTypes.Count - 1).Select(o => ConvertType(o)).ToArray();
                }
            }

            writer.Write("function (");

            int paramNum = 0;
            foreach (var param in lambdaExpression.Parameters)
            {
                if (paramNum > 0)
                    writer.Write(", ");

                writer.Write(param.ParameterName);
                writer.Write(":");
                writer.Write(parameterTypes[paramNum]);

                paramNum++;
            }

            writer.Write("):");
            writer.Write(returnType);    
            writer.Write("\r\n");
            writer.WriteOpenBrace();

            InLambda++;

            if (!lambdaExpression.ExpressionBody.IsNull)
            {
                writer.WriteIndent();
                writer.Write("return ");
                WriteStatement(writer, lambdaExpression.ExpressionBody);
                writer.Write(";\r\n");
            }
            else if (!lambdaExpression.StatementBody.IsNull)
                WriteStatement(writer, lambdaExpression.StatementBody);
            else
                throw new Exception("No lambda body found");

            InLambda--;

            writer.WriteCloseBrace();
            writer.WriteIndent();
        }

        private void WriteBreakStatement(ActionScriptWriter writer, BreakStatement breakStatement)
        {
            writer.WriteLine("break;");
        }

        private void WriteForStatement(ActionScriptWriter writer, ForStatement forStatement)
        {
            writer.WriteIndent();
            writer.Write("for (");

            var firstInit = true;
            foreach (var init in forStatement.Initializers)
            {
                if (firstInit)
                    firstInit = false;
                else
                    writer.Write(", ");

                WriteStatement(writer, init, true);
            }

            writer.Write("; ");
            WriteStatement(writer, forStatement.Condition);
            writer.Write("; ");

            var firstIterator = true;
            foreach (var iterator in forStatement.Iterator)
            {
                if (firstIterator)
                    firstIterator = false;
                else
                    writer.Write(", ");
                WriteStatement(writer, iterator, true);
            }

            writer.Write(")\r\n");
            writer.WriteOpenBrace();
            WriteStatement(writer, forStatement.EmbeddedStatement);
            writer.WriteCloseBrace();
        }

        private void WriteDoLoopStatement(ActionScriptWriter writer, DoLoopStatement doLoopStatement)
        {
            if (doLoopStatement.ConditionPosition == ConditionPosition.Start)
            {
                writer.WriteIndent();
                writer.Write("while (");
                WriteStatement(writer, doLoopStatement.Condition);
                writer.Write(")\r\n");
                writer.WriteOpenBrace();
                WriteStatement(writer, doLoopStatement.EmbeddedStatement);
                writer.WriteCloseBrace();
            }
            else
            {
                writer.WriteLine("do");
                writer.WriteOpenBrace();
                WriteStatement(writer, doLoopStatement.EmbeddedStatement);
                writer.WriteCloseBrace();
                writer.WriteIndent();
                writer.Write("while (");
                WriteStatement(writer, doLoopStatement.Condition);
                writer.Write(");\r\n");
            }
        }

        private void WriteIndexerExpression(ActionScriptWriter writer, IndexerExpression indexerExpression)
        {
            if (indexerExpression.Indexes.Count > 1)
                throw new Exception("Multiple indexers?");

            var expression = indexerExpression.Indexes.Single();
            WriteStatement(writer, indexerExpression.TargetObject);

            if (indexerExpression.TargetObject is IdentifierExpression)
            {
                TypeReference typeRef;
                if (Utility.TryFindType(indexerExpression.TargetObject.As<IdentifierExpression>(), out typeRef))
                {
                    if (typeRef.Type == "Dictionary")
                    {
                        writer.Write(".GetValue(");
                        WriteStatement(writer, expression);
                        writer.Write(")");
                        return;
                    }
                }
            }

            writer.Write("[");
            WriteStatement(writer, expression);
            writer.Write("]");
        }

        private void WriteBlockStatement(ActionScriptWriter writer, BlockStatement blockStatement)
        {
            foreach (var child in blockStatement.Children)
                WriteStatement(writer, child);
        }

        private void WriteForeachStatement(ActionScriptWriter writer, ForeachStatement foreachStatement)
        {
            writer.WriteIndent();
            writer.Write("for each (var ");
            writer.Write(foreachStatement.VariableName);
            writer.Write(":");
            writer.Write(ConvertType(foreachStatement.TypeReference));
            writer.Write(" in ");
            WriteStatement(writer, foreachStatement.Expression);

            TypeReference typeRef;
            if (foreachStatement.Expression is IdentifierExpression && Utility.TryFindType(foreachStatement.Expression.As<IdentifierExpression>(), out typeRef) && typeRef.Type == "HashSet")
                writer.Write(".Values()");

            writer.Write(")\r\n");
            writer.WriteOpenBrace();
            WriteStatement(writer, foreachStatement.EmbeddedStatement);
            writer.WriteCloseBrace();
        }

        private void WriteArrayCreateExpression(ActionScriptWriter writer, ArrayCreateExpression arrayCreateExpression)
        {
            if (arrayCreateExpression.CreateType.Type == "System.Byte")
            {
                if (arrayCreateExpression.ArrayInitializer.CreateExpressions.Count > 0)
                    throw new Exception("Cannot use array initialization syntax for byte arrays");

                writer.Write("new ByteArray()");
            }
            else
            {
                writer.Write("[ ");

                bool firstItem = true;
                foreach (var arrayItem in arrayCreateExpression.ArrayInitializer.CreateExpressions)
                {
                    if (firstItem)
                        firstItem = false;
                    else
                        writer.Write(", ");

                    WriteStatement(writer, arrayItem);
                }

                writer.Write(" ]");
            }
        }

        private void WriteReturnStatement(ActionScriptWriter writer, ReturnStatement returnStatement)
        {
            writer.WriteIndent();
            writer.Write("return");

            if (!returnStatement.Expression.IsNull)
            {
                writer.Write(" ");
                WriteStatement(writer, returnStatement.Expression);
            }
            writer.Write(";\r\n");
        }

        private void WriteUnaryOperatorExpression(ActionScriptWriter writer, UnaryOperatorExpression unaryOperatorExpression)
        {
            switch (unaryOperatorExpression.Op)
            {
                case UnaryOperatorType.Not: writer.Write("!"); break;
                case UnaryOperatorType.Decrement: writer.Write("--"); break;
                case UnaryOperatorType.Increment: writer.Write("++"); break;
                case UnaryOperatorType.Minus: writer.Write("-"); break;
                case UnaryOperatorType.Plus: writer.Write("+"); break;
                case UnaryOperatorType.BitNot: writer.Write("~"); break;

                case UnaryOperatorType.Dereference:
                case UnaryOperatorType.AddressOf:
                    throw new Exception("Not supported: " + unaryOperatorExpression.Op + " found in " + Utility.Descriptor(unaryOperatorExpression));
            }

            WriteStatement(writer, unaryOperatorExpression.Expression);

            switch (unaryOperatorExpression.Op)
            {
                case UnaryOperatorType.PostDecrement: writer.Write("--"); break;
                case UnaryOperatorType.PostIncrement: writer.Write("++"); break;
            }
        }

        public bool IsExtensionMethod(string methodName, Expression expression, out string methodNamespace)
        {
            var ext = ConvertingExtensionMethods.FirstOrDefault(o => o.Name == methodName);
            if (ext != null)
            {
                methodNamespace = ext.Parent.Parent.As<NamespaceDeclaration>().Name + "." + ext.Parent.As<TypeDeclaration>().Name;
                return true;
            }

            methodNamespace = null;
            return false;
        }


        private void WriteBinaryOperatorExpression(ActionScriptWriter writer, BinaryOperatorExpression binaryOperatorExpression)
        {
            Func<string> opString = () =>
                {
                    switch (binaryOperatorExpression.Op)
                    {
                        case BinaryOperatorType.Add: return "+";
                        case BinaryOperatorType.Equality: return "==";
                        case BinaryOperatorType.Concat: return "+";
                        case BinaryOperatorType.LessThan: return "<";
                        case BinaryOperatorType.GreaterThan: return ">";
                        case BinaryOperatorType.LessThanOrEqual: return "<=";
                        case BinaryOperatorType.GreaterThanOrEqual: return ">=";
                        case BinaryOperatorType.InEquality: return "!=";
                        case BinaryOperatorType.Subtract: return "-";
                        case BinaryOperatorType.LogicalAnd: return "&&";
                        case BinaryOperatorType.LogicalOr: return "||";
                        case BinaryOperatorType.Modulus: return "%";
                        case BinaryOperatorType.BitwiseOr: return "|";
                        case BinaryOperatorType.ShiftLeft: return "<<";
                        case BinaryOperatorType.ShiftRight: return ">>";
                        case BinaryOperatorType.Multiply: return "*";
                        case BinaryOperatorType.Divide: return "/";
                        case BinaryOperatorType.BitwiseAnd: return "&";
                        default:
                            throw new Exception("Need handler for " + binaryOperatorExpression.Op + " at " + Utility.Descriptor(binaryOperatorExpression));
                    }
                };

            WriteStatement(writer, binaryOperatorExpression.Left);
            writer.Write(" ");
            writer.Write(opString());
            writer.Write(" ");
            WriteStatement(writer, binaryOperatorExpression.Right);
        }

        private void WriteIfElseStatement(ActionScriptWriter writer, IfElseStatement ifElseStatement)
        {
            writer.WriteIndent();
            writer.Write("if (");
            WriteStatement(writer, ifElseStatement.Condition);
            writer.Write(")\r\n");
            writer.WriteOpenBrace();

            foreach (var statement in ifElseStatement.TrueStatement)
                WriteStatement(writer, statement);

            writer.WriteCloseBrace();

            if (ifElseStatement.HasElseIfSections)
                foreach (var elseIf in ifElseStatement.ElseIfSections)
                {
                    writer.WriteIndent();
                    writer.Write("else if (");
                    WriteStatement(writer, elseIf.Condition);
                    writer.Write(")\r\n");
                    writer.WriteOpenBrace();

                    WriteStatement(writer, elseIf.EmbeddedStatement);

                    writer.WriteCloseBrace();
                }

            if (ifElseStatement.HasElseStatements)
            {
                writer.WriteLine("else");
                writer.WriteOpenBrace();

                foreach (var statement in ifElseStatement.FalseStatement)
                    WriteStatement(writer, statement);

                writer.WriteCloseBrace();
            }
        }

        public string ConvertType(TypeReference type)
        {
            if (type.IsArrayType && type.Type == "System.Byte")
                return "ByteArray";
            if (type.IsArrayType)
                return "Array";

            var translation = Translations.Translation.GetTranslation(TranslationDocs, Translations.Translation.TranslationType.Type, type.Type, type) as Translations.Type;

            if (translation != null)
                return translation.ReplaceWith;

            if (DelegateNames.Contains(type.Type))
                return "Function"; //All delegates become functions

            if (EnumNames.Contains(type.Type))
                return "int"; //Enums are all ints

            if (type.Type == "Action" || type.Type == "Func")
                return "Function";

            //Handle generic types that we can convert
            switch (type.Type)
            {
                case "IEnumerable":
                case "List":
                case "Queue":
                case "Stack":
                    return "Array";
                case "HashSet":
                    return "HashSet";
                case "Dictionary":
                    return "CSDictionary";
                case "KeyValuePair":
                    return "KeyValuePair";
                case "System.Nullable":
                case "Nullable":
                    return "Nullable_" + ConvertType(type.GenericTypes.Single());
            }

            //All other generic types just use dynamic
            if (type.GenericTypes.Count > 0)
                return "*";

            //Handle non-generic types
            switch (type.Type)
            {
                case "System.UInt32": return "uint";
                case "System.Void": return "void";
                case "System.Boolean": return "Boolean";
                case "System.Object": return "Object";

                case "System.Single":
                case "System.Double":
                case "System.UInt64":
                case "System.Int64":
                case "Int64":
                case "UInt64":
                case "Single":
                case "Double":
                    return "Number";

                case "System.String":
                    return "String";

                case "System.Char":
                case "System.UInt16":
                case "System.Int16":
                case "System.Byte":
                case "System.Int32":
                case "Int32":
                case "Byte":
                case "Int16":
                case "UInt16":
                case "Char":
                    return "int";

                case "var": return "*";
                default:
                    //Determine if it's a generic type, such as T in Method<T>
                    var method = Utility.GetMethod(type);
                    if (method != null && method.Templates.Any(o => o.Name == type.Type))
                        return "*";

                    //This type does not get translated and gets used as-is
                    return type.Type;
            }
        }

        private void WriteLocalVariableDeclaration(ActionScriptWriter writer, LocalVariableDeclaration localVariableDeclaration, bool suppressSemicolon)
        {
            string type = ConvertType(Utility.DetermineType(localVariableDeclaration));
            
            foreach(var variable in localVariableDeclaration.Variables)
            {
                if (!suppressSemicolon)
                    writer.WriteIndent();

                writer.Write("var ");
                writer.Write(variable.Name);
                writer.Write(":" + type);

                if (!variable.Initializer.IsNull)
                {
                    writer.Write(" = ");
                    WriteStatement(writer, variable.Initializer);
                }

                if (!suppressSemicolon)
                    writer.Write(";\r\n");
            }
        }

        private void WritePrimitiveExpression(ActionScriptWriter writer, PrimitiveExpression primitiveExpression)
        {
            switch (primitiveExpression.LiteralFormat)
            {
                case LiteralFormat.VerbatimStringLiteral:
                    var raw = primitiveExpression.StringValue;

                    if (!raw.StartsWith("@"))
                        throw new Exception("Expected Verbatim to start with @");

                    writer.Write(raw.Substring(1).Replace("\\", "\\\\").Replace("\"\"", "\\\""));
                    break;
                default:
                    writer.Write(primitiveExpression.StringValue);
                    break;
            }            
        }

        private void WriteExpressionStatement(ActionScriptWriter writer, ExpressionStatement expressionStatement, bool suppressSemicolon)
        {
            if (!suppressSemicolon)
                writer.WriteIndent();

            WriteStatement(writer, expressionStatement.Expression);

            if (!suppressSemicolon)
                writer.Write(";\r\n");
        }

        private void WriteAssignmentExpression(ActionScriptWriter writer, AssignmentExpression assignmentExpression)
        {
            Func<string> determineOperator = () =>
                {
                    switch (assignmentExpression.Op)
                    {
                        case AssignmentOperatorType.Assign: return "=";
                        case AssignmentOperatorType.Add: return "+=";
                        case AssignmentOperatorType.ConcatString: return "+=";
                        case AssignmentOperatorType.Subtract: return "-=";
                        case AssignmentOperatorType.Modulus: return "%=";
                        case AssignmentOperatorType.Divide: return "/=";
                        case AssignmentOperatorType.DivideInteger: return "/=";
                        case AssignmentOperatorType.Multiply: return "*=";
                        case AssignmentOperatorType.BitwiseOr: return "|=";
                        default:
                            throw new Exception("Need handler for " + assignmentExpression.Op.ToString());
                    }
                };

            WriteStatement(writer, assignmentExpression.Left);
            writer.Write(" ");
            writer.Write(determineOperator());
            writer.Write(" ");
            WriteStatement(writer, assignmentExpression.Right);
        }

        private void WriteMemberReferenceExpression(ActionScriptWriter writer, MemberReferenceExpression memberReferenceExpression)
        {
            string memberName = memberReferenceExpression.MemberName;

            var translation = Translations.Translation.GetTranslation(TranslationDocs, Translations.Translation.TranslationType.Property, memberName, memberReferenceExpression) as Translations.Property;

            if (translation != null)
                memberName = translation.ReplaceWith;

            if (memberReferenceExpression.TargetObject is TypeReferenceExpression && (memberName == "MaxValue" || memberName == "MinValue" || memberName == "Empty"))
            {
                //Support int.MaxValue/int.MaxValue/etc
                var typeRef = memberReferenceExpression.TargetObject.As<TypeReferenceExpression>();

                if (memberName == "Empty" && typeRef.TypeReference.Type == "System.String")
                    writer.Write("\"\"");
                else
                    writer.Write(Type.GetType(typeRef.TypeReference.Type).GetField(memberName).GetValue(null).ToString());
            }
            else
            {
                
                WriteStatement(writer, memberReferenceExpression.TargetObject);
                writer.Write(".");
                writer.Write(memberName);
            }
        }

        private void WriteInvocationExpression(ActionScriptWriter writer, InvocationExpression invocationExpression)
        {
            bool isExtensionMethod;
            Translations.Method translate = null;

            if (!(invocationExpression.TargetObject is MemberReferenceExpression))
            {
                WriteStatement(writer, invocationExpression.TargetObject);
                isExtensionMethod = false;
            }
            else
            {
                var memberReferenceExpression = invocationExpression.TargetObject.As<MemberReferenceExpression>();

                translate = Translations.Translation.GetTranslation(TranslationDocs, Translations.Translation.TranslationType.Method, memberReferenceExpression.MemberName, memberReferenceExpression.TargetObject) as Translations.Method;
                string methodName;
                string extensionNamespace;

                if (translate == null)
                    methodName = memberReferenceExpression.MemberName;
                else
                    methodName = translate.ReplaceWith ?? memberReferenceExpression.MemberName;

                if (translate != null && translate.IsExtensionMethod)
                {
                    isExtensionMethod = true;
                    extensionNamespace = translate.ExtensionNamespace;
                }
                else if (IsExtensionMethod(memberReferenceExpression.MemberName, memberReferenceExpression.TargetObject, out extensionNamespace))
                    isExtensionMethod = true;
                else
                    isExtensionMethod = false;

                if (isExtensionMethod)
                {
                    writer.Write(extensionNamespace);
                    writer.Write(".");
                    writer.Write(methodName);

                    if (translate == null || !translate.SkipExtensionParameter)
                    {
                        //We must try writing the statement.  If it ends up being the same as extensionNamespace, that means this extension method was called in a non-extension way and we should omit the extension parameter.
                        var extensionParameterSb = new StringBuilder();
                        using (var extensionParameterWriter = new ActionScriptWriter(new StringWriter(extensionParameterSb)))
                            WriteStatement(extensionParameterWriter, memberReferenceExpression.TargetObject);

                        if (extensionNamespace.EndsWith(extensionParameterSb.ToString()))
                            isExtensionMethod = false;
                        else
                        {
                            writer.Write("(");
                            writer.Write(extensionParameterSb.ToString());
                        }
                    }
                    else
                        isExtensionMethod = false;
                }
                else
                {
                    if (memberReferenceExpression.TargetObject is TypeReferenceExpression)
                    {
                        switch (methodName)
                        {
                            case "Parse":
                                writer.Write(ConvertType(memberReferenceExpression.TargetObject.As<TypeReferenceExpression>().TypeReference));
                                break;
                            default:
                                throw new Exception("Need handler for " + methodName);
                        }
                    }
                    else
                    {
                        WriteStatement(writer, memberReferenceExpression.TargetObject);
                        writer.Write(".");
                        writer.Write(methodName);
                    }
                }
            }

            if (!isExtensionMethod)
                writer.Write("(");

            var firstArg = !isExtensionMethod;
            foreach (var arg in TranslateParameters(translate, invocationExpression.Arguments, invocationExpression))
            {
                if (firstArg)
                    firstArg = false;
                else
                    writer.Write(", ");

                WriteStatement(writer, arg);
            }


            writer.Write(")");
        }
    }
}
